﻿namespace LivePatterns
{
	using System;
	using System.Collections;
	using System.Collections.Generic;
	using System.Linq;
	using System.Reflection;
	using System.Reflection.Emit;

	public static partial class Build
	{
		/// <summary>
		/// Handles multiple objects as one, performs every operation on each object.
		/// </summary>
		/// <typeparam name="T"></typeparam>
		/// <param name="objects">The objects to multicast operations to.</param>
		/// <returns>Returns a proxy instance that multicasts all operations.</returns>
		/// <remarks>Reading data is not allowed.</remarks>
		/// <example>
		/// IRecipient[] recipients = session.Recipients;
		/// IRecipient all = Build.Multicast&lt;IRecipient&gt;(recipients);
		/// all.Say("Hello");
		/// </example>
		public static T Multicast<T>(IEnumerable<T> objects) where T : class
		{
			if (!typeof(T).IsInterface)
				throw new ArgumentException("Only interface types are supported.");

			if (objects == null)
				throw new ArgumentNullException("objects");

			if (!objects.Any())
				throw new ArgumentException("The provided collection contains no elements.", "objects");
            
			// resolve type from cache, if already built
			if (_multicastTypeCache.ContainsKey(typeof(T)))
				return Activator.CreateInstance(_multicastTypeCache[typeof(T)], objects) as T;

			TypeBuilder typeBuilder = moduleBuilder.DefineType(String.Format("{0}_Multicast_{1}", typeof(T).Name, Guid.NewGuid().ToString().Replace("-", "")), TypeAttributes.Class | TypeAttributes.Public);
			typeBuilder.AddInterfaceImplementation(typeof(T));

			// create a field for storing the object instances
			FieldBuilder fieldBuilder = typeBuilder.DefineField("innerObjects", typeof(T), FieldAttributes.Family);

			// create a construtor, in which we can pass the instances
			ConstructorBuilder constructorBuilder = typeBuilder.DefineConstructor(MethodAttributes.Public, CallingConventions.Standard, new Type[] { typeof(IEnumerable<T>) });
			ILGenerator cilg = constructorBuilder.GetILGenerator();
			cilg.ConstructObject();
			cilg.Emit(OpCodes.Ldarg_0);
			cilg.Emit(OpCodes.Ldarg_1);
			cilg.Emit(OpCodes.Stfld, fieldBuilder);
			cilg.Emit(OpCodes.Ret);

			// implement fields
			foreach (FieldInfo fi in typeof(T).GetFields(BindingFlags.Public | BindingFlags.Instance).Where(m => !m.IsSpecialName))
			{
				FieldBuilder fb = typeBuilder.DefineField(fi.Name, fi.FieldType, FieldAttributes.Public | FieldAttributes.Literal);
			}

			// implement properties
			foreach (PropertyInfo pi in typeof(T).GetAllVisibleProperties())
			{
				PropertyBuilder pb = typeBuilder.DefineProperty(pi.Name, PropertyAttributes.None, pi.PropertyType, new Type[0]);
				MethodBuilder methodBuilder;
				ILGenerator ilGenerator;

				// get method
				if (pi.CanRead)
				{
					MethodInfo getMethod = pi.GetGetMethod();
					methodBuilder = typeBuilder.DefineMethod(
						getMethod.Name, _methodAttributes, getMethod.ReturnType, getMethod.GetParameters().Select(p => p.ParameterType).ToArray()
					);

					ilGenerator = methodBuilder.GetILGenerator();
					ilGenerator.ThrowException(typeof(InvalidOperationException), "Getting property values is not supported.");
					pb.SetGetMethod(methodBuilder);
				}

				// set method
				if (pi.CanWrite)
				{
					MethodInfo setMethod = pi.GetSetMethod();
					methodBuilder = typeBuilder.DefineMethod(
						setMethod.Name, _methodAttributes, setMethod.ReturnType, setMethod.GetParameters().Select(p => p.ParameterType).ToArray()
					);
					methodBuilder.DefineParameter(1, ParameterAttributes.None, "value");

					ilGenerator = methodBuilder.GetILGenerator();
					Label fe = ilGenerator.DefineLabel(), fee = ilGenerator.DefineLabel();
					ilGenerator.DeclareLocal(typeof(IEnumerator<T>));
					ilGenerator.Emit(OpCodes.Ldarg_0);
					ilGenerator.Emit(OpCodes.Ldfld, fieldBuilder);
					ilGenerator.Emit(OpCodes.Callvirt, typeof(IEnumerable<T>).GetMethod("GetEnumerator"));
					ilGenerator.Emit(OpCodes.Stloc_0);
					ilGenerator.Emit(OpCodes.Br_S, fee);
					ilGenerator.MarkLabel(fe);
					ilGenerator.Emit(OpCodes.Ldloc_0);
					ilGenerator.Emit(OpCodes.Callvirt, typeof(IEnumerator<T>).GetProperty("Current").GetGetMethod());
					ilGenerator.Emit(OpCodes.Ldarg_1);
					ilGenerator.Emit(OpCodes.Callvirt, setMethod);
					ilGenerator.MarkLabel(fee);
					ilGenerator.Emit(OpCodes.Ldloc_0);
					ilGenerator.Emit(OpCodes.Callvirt, typeof(IEnumerator).GetMethod("MoveNext"));
					ilGenerator.Emit(OpCodes.Brtrue_S, fe);
					ilGenerator.Return();
					pb.SetSetMethod(methodBuilder);
				}
			}

			// implement methods
			foreach (MethodInfo mi in typeof(T).GetAllVisibleMethods())
			{
				MethodBuilder methodBuilder = typeBuilder.DefineMethod(
					mi.Name, MethodAttributes.Public | MethodAttributes.Virtual, mi.ReturnType, mi.GetParameters().Select(p => p.ParameterType).ToArray()
				);

				ILGenerator ilGenerator = methodBuilder.GetILGenerator();

				// methods with non-void return type are not supported.
				if (mi.ReturnType != typeof(void))
				{
					ilGenerator.ThrowException(typeof(NotSupportedException), "Methods with non-void return type are not supported.");
					continue;
				}

				Label fe = ilGenerator.DefineLabel(), fee = ilGenerator.DefineLabel();
				ilGenerator.DeclareLocal(typeof(IEnumerator<T>));
				ilGenerator.Emit(OpCodes.Ldarg_0);
				ilGenerator.Emit(OpCodes.Ldfld, fieldBuilder);
				ilGenerator.Emit(OpCodes.Callvirt, typeof(IEnumerable<T>).GetMethod("GetEnumerator"));
				ilGenerator.Emit(OpCodes.Stloc_0);
				ilGenerator.Emit(OpCodes.Br_S, fee);
				ilGenerator.MarkLabel(fe);
				ilGenerator.Emit(OpCodes.Ldloc_0);
				ilGenerator.Emit(OpCodes.Callvirt, typeof(IEnumerator<T>).GetProperty("Current").GetGetMethod());
				ilGenerator.LoadAllArguments(mi);
				ilGenerator.Emit(OpCodes.Callvirt, mi);
				ilGenerator.MarkLabel(fee);
				ilGenerator.Emit(OpCodes.Ldloc_0);
				ilGenerator.Emit(OpCodes.Callvirt, typeof(IEnumerator).GetMethod("MoveNext"));
				ilGenerator.Emit(OpCodes.Brtrue_S, fe);
				ilGenerator.Return();
			}

			// implement events
			foreach (EventInfo ei in typeof(T).GetAllVisibleEvents())
			{
				// define event
				EventBuilder eb = typeBuilder.DefineEvent(ei.Name, EventAttributes.None, ei.EventHandlerType);
				MethodBuilder methodBuilder;
				ILGenerator ilGenerator;

				//
				// implement add method
				//
				MethodInfo addMethod = ei.GetAddMethod();
				methodBuilder = typeBuilder.DefineMethod(
					addMethod.Name, _methodAttributes, addMethod.ReturnType, addMethod.GetParameters().Select(p => p.ParameterType).ToArray()
				);

				ilGenerator = methodBuilder.GetILGenerator();
				Label fe = ilGenerator.DefineLabel(), fee = ilGenerator.DefineLabel();
				ilGenerator.DeclareLocal(typeof(IEnumerator<T>));
				ilGenerator.Emit(OpCodes.Ldarg_0);
				ilGenerator.Emit(OpCodes.Ldfld, fieldBuilder);
				ilGenerator.Emit(OpCodes.Callvirt, typeof(IEnumerable<T>).GetMethod("GetEnumerator"));
				ilGenerator.Emit(OpCodes.Stloc_0);
				ilGenerator.Emit(OpCodes.Br_S, fee);
				ilGenerator.MarkLabel(fe);
				ilGenerator.Emit(OpCodes.Ldloc_0);
				ilGenerator.Emit(OpCodes.Callvirt, typeof(IEnumerator<T>).GetProperty("Current").GetGetMethod());
				ilGenerator.Emit(OpCodes.Ldarg_1);
				ilGenerator.Emit(OpCodes.Callvirt, addMethod);
				ilGenerator.MarkLabel(fee);
				ilGenerator.Emit(OpCodes.Ldloc_0);
				ilGenerator.Emit(OpCodes.Callvirt, typeof(IEnumerator).GetMethod("MoveNext"));
				ilGenerator.Emit(OpCodes.Brtrue_S, fe);
				ilGenerator.Return();
				eb.SetAddOnMethod(methodBuilder);

				//
				// implement remove method
				//
				MethodInfo removeMethod = ei.GetRemoveMethod();
				methodBuilder = typeBuilder.DefineMethod(
					removeMethod.Name, _methodAttributes, removeMethod.ReturnType, removeMethod.GetParameters().Select(p => p.ParameterType).ToArray()
				);

				ilGenerator = methodBuilder.GetILGenerator();
				fe = ilGenerator.DefineLabel(); fee = ilGenerator.DefineLabel();
				ilGenerator.DeclareLocal(typeof(IEnumerator<T>));
				ilGenerator.Emit(OpCodes.Ldarg_0);
				ilGenerator.Emit(OpCodes.Ldfld, fieldBuilder);
				ilGenerator.Emit(OpCodes.Callvirt, typeof(IEnumerable<T>).GetMethod("GetEnumerator"));
				ilGenerator.Emit(OpCodes.Stloc_0);
				ilGenerator.Emit(OpCodes.Br_S, fee);
				ilGenerator.MarkLabel(fe);
				ilGenerator.Emit(OpCodes.Ldloc_0);
				ilGenerator.Emit(OpCodes.Callvirt, typeof(IEnumerator<T>).GetProperty("Current").GetGetMethod());
				ilGenerator.Emit(OpCodes.Ldarg_1);
				ilGenerator.Emit(OpCodes.Callvirt, removeMethod);
				ilGenerator.MarkLabel(fee);
				ilGenerator.Emit(OpCodes.Ldloc_0);
				ilGenerator.Emit(OpCodes.Callvirt, typeof(IEnumerator).GetMethod("MoveNext"));
				ilGenerator.Emit(OpCodes.Brtrue_S, fe);
				ilGenerator.Return();
				eb.SetRemoveOnMethod(methodBuilder);
			}

			return Activator.CreateInstance(typeBuilder.CreateType(), objects) as T;
		}

        
		private static readonly IDictionary<Type, Type> _multicastTypeCache = new Dictionary<Type, Type>();
	}
}